// Copyright 2021 Proyectos y Sistemas de Mantenimiento SL (eProsima).
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ------------------------------------------------------------------
// Modification history:
// feature: support certificate chain verification
// ------------------------------------------------------------------

#include <security/artifact_providers/FileProvider.hpp>

#include <cassert>
#include <cstring>
#include <iostream>

#define S1(x) #x
#define S2(x) S1(x)
#define LOCATION " (" __FILE__ ":" S2(__LINE__) ")"
#define _SecurityException_(str) SecurityException(std::string(str) + LOCATION)

namespace evbs {
namespace ertps {
namespace rtps {
namespace security {
namespace detail {

X509_STORE* FileProvider::load_ca(const std::string& ca, bool& there_are_crls, std::string& ca_sn, std::string& ca_algo,
                                  std::function<bool(X509*, std::string&, SecurityException&)> get_signature_algorithm,
                                  SecurityException& exception) {
    X509_STORE* store = X509_STORE_new();

    if (store != nullptr) {
        BIO* in = BIO_new(BIO_s_file());

        if (in != nullptr) {
            if (BIO_read_filename(in, ca.substr(7).c_str()) > 0) {
                STACK_OF(X509_INFO)* inf = PEM_X509_INFO_read_bio(in, NULL, NULL, NULL);

                if (inf != nullptr) {
                    int i, count = 0;
                    there_are_crls = false;

                    for (i = 0; i < sk_X509_INFO_num(inf); i++) {
                        X509_INFO* itmp = sk_X509_INFO_value(inf, i);

                        if (itmp->x509) {
                            // Retrieve subject name for future use.
                            if (ca_sn.empty()) {
                                X509_NAME* ca_subject_name = X509_get_subject_name(itmp->x509);
                                assert(ca_subject_name != nullptr);
                                char* ca_subject_name_str = X509_NAME_oneline(ca_subject_name, 0, 0);
                                assert(ca_subject_name_str != nullptr);
                                ca_sn = ca_subject_name_str;
                                OPENSSL_free(ca_subject_name_str);
                            }

                            // Retrieve signature algorithm
                            if (ca_algo.empty()) {
                                if (get_signature_algorithm(itmp->x509, ca_algo, exception)) {
                                    (void)X509_STORE_add_cert(store, itmp->x509);
                                    count++;
                                }
                            } else {
                                (void)X509_STORE_add_cert(store, itmp->x509);
                                count++;
                            }
                        }
                        if (itmp->crl) {
                            (void)X509_STORE_add_crl(store, itmp->crl);
                            there_are_crls = true;
                        }
                    }

                    sk_X509_INFO_pop_free(inf, X509_INFO_free);

                    if (count > 0) {
                        (void)BIO_free(in);

                        return store;
                    }
                } else {
                    exception = _SecurityException_(std::string("OpenSSL library cannot read X509 info in file ") +
                                                    ca.substr(7));
                }
            } else {
                exception = _SecurityException_(std::string("OpenSSL library cannot read file ") + ca.substr(7));
            }

            (void)BIO_free(in);
        } else {
            exception = _SecurityException_("OpenSSL library cannot allocate file");
        }

        X509_STORE_free(store);
    } else {
        exception = _SecurityException_("Creation of X509 storage");
    }

    return nullptr;
}

X509_STORE* FileProvider::load_ca_from_string(
    const std::string& ca, bool& there_are_crls, std::string& ca_sn, std::string& ca_algo,
    std::function<bool(X509*, std::string&, SecurityException&)> get_signature_algorithm,
    SecurityException& exception) {
    X509_STORE* store = X509_STORE_new();

    if (!store) {
        exception = SecurityException("Creation of X509 storage failed");
        return nullptr;
    }

    BIO* in = BIO_new_mem_buf(ca.data(), static_cast<int>(ca.size()));
    if (!in) {
        X509_STORE_free(store);
        exception = SecurityException("OpenSSL library cannot allocate memory buffer");
        return nullptr;
    }

    STACK_OF(X509_INFO)* inf = PEM_X509_INFO_read_bio(in, nullptr, nullptr, nullptr);
    if (!inf) {
        BIO_free(in);
        X509_STORE_free(store);
        exception = SecurityException("OpenSSL library cannot read X509 info from memory buffer");
        return nullptr;
    }

    int count = 0;
    there_are_crls = false;

    for (int i = 0; i < sk_X509_INFO_num(inf); i++) {
        X509_INFO* itmp = sk_X509_INFO_value(inf, i);
        if (!itmp)
            continue;

        if (itmp->x509) {
            // Retrieve subject name
            if (ca_sn.empty()) {
                X509_NAME* ca_subject_name = X509_get_subject_name(itmp->x509);
                assert(ca_subject_name != nullptr);
                char* ca_subject_name_str = X509_NAME_oneline(ca_subject_name, nullptr, 0);
                assert(ca_subject_name_str != nullptr);
                ca_sn = ca_subject_name_str;
                OPENSSL_free(ca_subject_name_str);
            }

            // Retrieve signature algorithm
            if (ca_algo.empty()) {
                if (get_signature_algorithm(itmp->x509, ca_algo, exception)) {
                    (void)X509_STORE_add_cert(store, itmp->x509);
                    count++;
                }
            } else {
                (void)X509_STORE_add_cert(store, itmp->x509);
                count++;
            }
        }

        if (itmp->crl) {
            (void)X509_STORE_add_crl(store, itmp->crl);
            there_are_crls = true;
        }
    }

    sk_X509_INFO_pop_free(inf, X509_INFO_free);
    BIO_free(in);

    if (count > 0) {
        return store;
    } else {
        X509_STORE_free(store);
        exception = SecurityException("No valid certificates found");
        return nullptr;
    }
}

X509* FileProvider::load_certificate(const std::string& identity_cert, SecurityException& exception) {
    X509* returnedValue = nullptr;
    BIO* in = BIO_new(BIO_s_file());

    if (in != nullptr) {
        if (BIO_read_filename(in, identity_cert.substr(7).c_str()) > 0) {
            returnedValue = PEM_read_bio_X509_AUX(in, NULL, NULL, NULL);
        } else {
            exception = _SecurityException_(std::string("OpenSSL library cannot read file ") + identity_cert.substr(7));
        }

        (void)BIO_free(in);
    } else {
        exception = _SecurityException_("OpenSSL library cannot allocate file");
    }

    return returnedValue;
}

/**
 * @brief 解析包含多个证书的 PEM 格式 `device_cert` 并提取第一个证书作为设备证书
 * @param identity_cert 证书链（包含多个 PEM 证书）
 * @param intermediate_certs 存储中间证书链（不含根证书）
 * @return `X509*` 设备证书（leaf certificate），调用方需要 `X509_free`
 */
X509* FileProvider::load_certificate_from_string(const std::string& identity_cert, STACK_OF(X509) * *intermediate_certs,
                                                 SecurityException& exception) {
    X509* returnedValue = nullptr;
    X509* cert = nullptr;

    BIO* in = BIO_new_mem_buf(identity_cert.data(), static_cast<int>(identity_cert.size()));
    if (in != nullptr) {
        // 从内存缓冲读取证书
        returnedValue = PEM_read_bio_X509_AUX(in, nullptr, nullptr, nullptr);  // 第一个证书是设备证书
        if (!returnedValue) {
            exception = SecurityException("OpenSSL library cannot read certificate from provided data");
        } else {
            *intermediate_certs = sk_X509_new_null();
            while ((cert = PEM_read_bio_X509_AUX(in, nullptr, nullptr, nullptr))) {
                sk_X509_push(*intermediate_certs, cert);  // 中间证书链
            }
        }

        BIO_free(in);
    } else {
        exception = SecurityException("OpenSSL library cannot allocate memory for reading certificate");
    }
    return returnedValue;
}

static int private_key_password_callback(char* buf, int bufsize, int /*verify*/, const char* password) {
    assert(password != nullptr);

    int returnedValue = static_cast<int>(strlen(password));

    if (returnedValue > bufsize) {
        returnedValue = bufsize;
    }

    (void)memcpy(buf, password, returnedValue);
    return returnedValue;
}

EVP_PKEY* FileProvider::load_private_key(X509* certificate, const std::string& pkey, const std::string& password,
                                         SecurityException& exception) {
    EVP_PKEY* returnedValue = nullptr;
    BIO* in = BIO_new(BIO_s_file());

    if (in != nullptr) {
        if (BIO_read_filename(in, pkey.substr(7).c_str()) > 0) {
            returnedValue = PEM_read_bio_PrivateKey(
                in, NULL, reinterpret_cast<pem_password_cb*>(private_key_password_callback), (void*)password.c_str());

            // Verify private key.
            if (nullptr == returnedValue) {
                exception = _SecurityException_(std::string("Error obtaining private key ") + pkey.substr(7));
            } else if (!X509_check_private_key(certificate, returnedValue)) {
                exception = _SecurityException_(std::string("Error verifying private key ") + pkey.substr(7));
                EVP_PKEY_free(returnedValue);
                returnedValue = nullptr;
            }
        } else {
            exception = _SecurityException_(std::string("OpenSSL library cannot read file ") + pkey.substr(7));
        }

        (void)BIO_free(in);
    } else {
        exception = _SecurityException_("OpenSSL library cannot allocate file");
    }

    return returnedValue;
}

X509_CRL* FileProvider::load_crl(const std::string& identity_crl, SecurityException& exception) {
    X509_CRL* returnedValue = nullptr;

    BIO* in = BIO_new(BIO_s_file());

    if (in != nullptr) {
        if (BIO_read_filename(in, identity_crl.substr(7).c_str()) > 0) {
            returnedValue = PEM_read_bio_X509_CRL(in, NULL, NULL, NULL);
        } else {
            exception = _SecurityException_(std::string("OpenSSL library cannot read file ") + identity_crl.substr(7));
        }

        (void)BIO_free(in);
    } else {
        exception = _SecurityException_("OpenSSL library cannot allocate file");
    }

    return returnedValue;
}

X509_CRL* FileProvider::load_crl_from_string(const std::string& identity_crl, SecurityException& exception) {
    X509_CRL* returnedValue = nullptr;

    BIO* in = BIO_new_mem_buf(identity_crl.data(), static_cast<int>(identity_crl.size()));

    if (in != nullptr) {
        // 从内存缓冲读取 CRL
        returnedValue = PEM_read_bio_X509_CRL(in, nullptr, nullptr, nullptr);
        if (!returnedValue) {
            exception = SecurityException("OpenSSL library cannot read CRL from provided data");
        }

        BIO_free(in);
    } else {
        exception = SecurityException("OpenSSL library cannot allocate memory for reading CRL");
    }

    return returnedValue;
}

}  // namespace detail
}  // namespace security
}  // namespace rtps
}  // namespace ertps
}  // namespace evbs
